/*
 * Copyright 2016 Hippo Seven
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.hippo.ehviewer.download

import android.Manifest
import android.app.ForegroundServiceStartNotAllowedException
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.collection.LongSparseArray
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.savedstate.serialization.decodeFromSavedState
import androidx.savedstate.serialization.encodeToSavedState
import com.ehviewer.core.database.model.DownloadInfo
import com.ehviewer.core.i18n.R
import com.ehviewer.core.model.BaseGalleryInfo
import com.ehviewer.core.util.isAtLeastS
import com.ehviewer.core.util.logcat
import com.ehviewer.core.util.unsafeLazy
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.exception.FatalException
import com.hippo.ehviewer.client.exception.InsufficientGpException
import com.hippo.ehviewer.client.exception.IpBannedException
import com.hippo.ehviewer.client.exception.QuotaExceededException
import com.hippo.ehviewer.ui.MainActivity
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.ReadableTime
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch

class DownloadService :
    Service(),
    DownloadManager.DownloadListener,
    CoroutineScope {
    override val coroutineContext = Dispatchers.IO + SupervisorJob()
    private val deferredMgr = async { DownloadManager }
    private val notifyManager by unsafeLazy { NotificationManagerCompat.from(this) }
    private val downloadingNotification by unsafeLazy { initDownloadingNotification() }
    private val downloadedNotification by lazy { initDownloadedNotification() }
    private val fatalNotification by lazy { initFatalNotification() }
    private val channelId by unsafeLazy { "$packageName.download" }

    override fun onCreate() {
        notifyManager.createNotificationChannel(
            NotificationChannelCompat.Builder(channelId, NotificationManagerCompat.IMPORTANCE_LOW)
                .setName(getString(R.string.download_service)).build(),
        )
        downloadingNotification.builder.runCatching {
            setContentTitle(getString(R.string.download_service))
                .setContentText(null)
                .setSubText(null)
                .setProgress(0, 0, true)
            startForeground(ID_DOWNLOADING, build())
        }.onFailure {
            if (isAtLeastS && it is ForegroundServiceStartNotAllowedException) {
                logcat(it)
            } else {
                throw it
            }
        }
        launch {
            deferredMgr.await().setDownloadListener(this@DownloadService)
        }
    }

    override fun onDestroy() {
        val scope = this
        launch {
            deferredMgr.await().setDownloadListener(null)
            // Wait for the last notification to be posted
            delay(DELAY)
            scope.cancel()
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        launch { handleIntent(intent) }
        return START_STICKY
    }

    private suspend fun handleIntent(intent: Intent?) {
        when (intent?.action) {
            ACTION_START -> {
                val gi = intent.getBundleExtra(KEY_GALLERY_INFO) ?: return
                val label = intent.getStringExtra(KEY_LABEL)
                deferredMgr.await().startDownload(decodeFromSavedState(gi), label)
            }
            ACTION_START_RANGE -> {
                val gidList = intent.getLongArrayExtra(KEY_GID_LIST)
                if (gidList != null) {
                    deferredMgr.await().startRangeDownload(gidList)
                }
            }
            ACTION_START_ALL -> deferredMgr.await().startAllDownload()
            ACTION_STOP_ALL -> deferredMgr.await().stopAllDownload()
            ACTION_CLEAR -> clear()
        }
        checkStopSelf()
    }

    override fun onBind(intent: Intent) = null

    private fun initDownloadingNotification(): NotificationHandler {
        val stopAllIntent = Intent(this, DownloadService::class.java)
        stopAllIntent.action = ACTION_STOP_ALL
        val piStopAll =
            PendingIntent.getService(this, 0, stopAllIntent, PendingIntent.FLAG_IMMUTABLE)
        val builder = NotificationCompat.Builder(applicationContext, channelId)
            .setSmallIcon(android.R.drawable.stat_sys_download)
            .setOngoing(true)
            .setAutoCancel(false)
            .setCategory(NotificationCompat.CATEGORY_PROGRESS)
            .addAction(
                com.hippo.ehviewer.R.drawable.v_pause_x24,
                getString(R.string.stat_download_action_stop_all),
                piStopAll,
            )
            .setShowWhen(false)
        return NotificationHandler(this, notifyManager, builder, ID_DOWNLOADING)
            .apply { launch { run() } }
    }

    private fun initDownloadedNotification(): NotificationHandler {
        val clearIntent = Intent(this, DownloadService::class.java)
        clearIntent.action = ACTION_CLEAR
        val piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE)
        val bundle = Bundle()
        bundle.putString(KEY_ACTION, ACTION_CLEAR_DOWNLOAD_SERVICE)
        val activityIntent = Intent(this, MainActivity::class.java)
        activityIntent.action = ACTION_START_DOWNLOADSCENE
        activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
        val piActivity = PendingIntent.getActivity(
            this,
            0,
            activityIntent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )
        val builder = NotificationCompat.Builder(applicationContext, channelId)
            .setSmallIcon(android.R.drawable.stat_sys_download_done)
            .setContentTitle(getString(R.string.stat_download_done_title))
            .setDeleteIntent(piClear)
            .setOngoing(false)
            .setAutoCancel(true)
            .setContentIntent(piActivity)
        return NotificationHandler(this, notifyManager, builder, ID_DOWNLOADED)
            .apply { launch { run() } }
    }

    private fun initFatalNotification(): NotificationHandler {
        val builder = NotificationCompat.Builder(applicationContext, channelId)
            .setSmallIcon(com.hippo.ehviewer.R.drawable.ic_baseline_warning_24)
            .setAutoCancel(true)
            .setOngoing(false)
            .setCategory(NotificationCompat.CATEGORY_ERROR)
        return NotificationHandler(this, notifyManager, builder, ID_FATAL)
            .apply { launch { run() } }
    }

    override fun onFatal(error: FatalException) {
        val (title, text) = when (error) {
            is IpBannedException -> R.string.error_ip_banned to error.message
            is InsufficientGpException -> R.string.insufficient_funds to error.message
            is QuotaExceededException -> R.string.stat_509_alert_title to getString(R.string.stat_509_alert_text)
        }
        fatalNotification.run {
            builder.setWhen(System.currentTimeMillis())
                .setContentTitle(getString(title))
                .setContentText(text)
                .setStyle(NotificationCompat.BigTextStyle().bigText(text))
            show()
        }
    }

    override fun onStart(info: DownloadInfo) {
        val bundle = Bundle()
        bundle.putLong(KEY_GID, info.gid)
        val activityIntent = Intent(this, MainActivity::class.java)
        activityIntent.action = ACTION_START_DOWNLOADSCENE
        activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
        val piActivity = PendingIntent.getActivity(
            this,
            0,
            activityIntent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )
        downloadingNotification.run {
            builder.setContentTitle(EhUtils.getSuitableTitle(info))
                .setContentText(null)
                .setSubText(null)
                .setProgress(0, 0, true)
                .setContentIntent(piActivity)
            startForeground()
        }
    }

    override fun onDownload(info: DownloadInfo) {
        val speed = info.speed.coerceAtLeast(0)
        val speedText = FileUtils.humanReadableByteCount(speed) + "/s"
        val remaining = info.remaining
        val text = if (remaining >= 0) {
            val interval = ReadableTime.getShortTimeInterval(remaining)
            getString(R.string.download_speed_text_2, speedText, interval)
        } else {
            getString(R.string.download_speed_text, speedText)
        }
        downloadingNotification.run {
            builder.setContentTitle(EhUtils.getSuitableTitle(info))
                .setContentText(text)
                .setStyle(NotificationCompat.BigTextStyle().bigText(text))
                .setSubText(if (info.total == -1 || info.finished == -1) null else info.finished.toString() + "/" + info.total)
                .setProgress(info.total, info.finished, false)
            startForeground()
        }
    }

    override fun onFinish(info: DownloadInfo) {
        downloadingNotification.cancel()
        val finish = info.state == DownloadInfo.STATE_FINISH
        val gid = info.gid
        val index = sItemStateArray.indexOfKey(gid)
        if (index < 0) { // Not contain
            sItemStateArray.put(gid, finish)
            sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info))
            sDownloadedCount++
            if (finish) {
                sFinishedCount++
            } else {
                sFailedCount++
            }
        } else { // Contain
            val oldFinish = sItemStateArray.valueAt(index)
            sItemStateArray.put(gid, finish)
            sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info))
            if (oldFinish && !finish) {
                sFinishedCount--
                sFailedCount++
            } else if (!oldFinish && finish) {
                sFinishedCount++
                sFailedCount--
            }
        }
        val text: String
        val needStyle: Boolean
        if (sFinishedCount != 0 && sFailedCount == 0) {
            if (sFinishedCount == 1) {
                text = if (sItemTitleArray.size() >= 1) {
                    getString(
                        R.string.stat_download_done_line_succeeded,
                        sItemTitleArray.valueAt(0),
                    )
                } else {
                    logcat { "WTF, sItemTitleArray is null" }
                    getString(R.string.error_unknown)
                }
                needStyle = false
            } else {
                text = getString(R.string.stat_download_done_text_succeeded, sFinishedCount)
                needStyle = true
            }
        } else if (sFinishedCount == 0 && sFailedCount != 0) {
            if (sFailedCount == 1) {
                text = if (sItemTitleArray.size() >= 1) {
                    getString(
                        R.string.stat_download_done_line_failed,
                        sItemTitleArray.valueAt(0),
                    )
                } else {
                    logcat { "WTF, sItemTitleArray is null" }
                    getString(R.string.error_unknown)
                }
                needStyle = false
            } else {
                text = getString(R.string.stat_download_done_text_failed, sFailedCount)
                needStyle = true
            }
        } else {
            text = getString(R.string.stat_download_done_text_mix, sFinishedCount, sFailedCount)
            needStyle = true
        }
        val style: NotificationCompat.InboxStyle?
        if (needStyle) {
            style = NotificationCompat.InboxStyle()
            style.setBigContentTitle(getString(R.string.stat_download_done_title))
            val stateArray = sItemStateArray
            var i = 0
            val n = stateArray.size()
            while (i < n) {
                val id = stateArray.keyAt(i)
                val fin = stateArray.valueAt(i)
                val title = sItemTitleArray[id]
                if (title == null) {
                    i++
                    continue
                }
                style.addLine(
                    getString(
                        if (fin) R.string.stat_download_done_line_succeeded else R.string.stat_download_done_line_failed,
                        title,
                    ),
                )
                i++
            }
        } else {
            style = null
        }
        downloadedNotification.run {
            builder.setContentText(text)
                .setStyle(style)
                .setWhen(System.currentTimeMillis())
                .setNumber(sDownloadedCount)
            show()
        }
        checkStopSelf()
    }

    override fun onCancel(info: DownloadInfo) {
        downloadingNotification.cancel()
        checkStopSelf()
    }

    private fun checkStopSelf() {
        launch {
            if (deferredMgr.await().isIdle) {
                stopSelf()
            }
        }
    }

    private class NotificationHandler(
        private val service: Service,
        private val notifyManager: NotificationManagerCompat,
        val builder: NotificationCompat.Builder,
        private val id: Int,
    ) {
        private val channel = Channel<Ops>(Channel.CONFLATED)

        fun show() {
            channel.trySend(Ops.Notify)
        }

        fun cancel() {
            channel.trySend(Ops.Cancel)
        }

        fun startForeground() {
            channel.trySend(Ops.StartForeground)
        }

        suspend fun run() {
            channel.receiveAsFlow().sample(DELAY).collect {
                when (it) {
                    Ops.Notify -> {
                        if (ActivityCompat.checkSelfPermission(
                                service,
                                Manifest.permission.POST_NOTIFICATIONS,
                            ) == PackageManager.PERMISSION_GRANTED
                        ) {
                            notifyManager.notify(id, builder.build())
                        }
                    }
                    Ops.Cancel -> notifyManager.cancel(id)
                    Ops.StartForeground -> service.startForeground(id, builder.build())
                }
            }
        }

        private enum class Ops {
            Notify,
            Cancel,
            StartForeground,
        }
    }

    companion object {
        const val ACTION_START_DOWNLOADSCENE = "start_download_scene"
        const val ACTION_START_DOWNLOADSCENE_ARGS = "start_download_scene_args"

        const val ACTION_START = "start"
        const val ACTION_START_RANGE = "start_range"
        const val ACTION_START_ALL = "start_all"
        const val ACTION_STOP_ALL = "stop_all"
        const val ACTION_CLEAR = "clear"
        const val KEY_GALLERY_INFO = "gallery_info"
        const val KEY_LABEL = "label"
        const val KEY_GID = "gid"
        const val KEY_GID_LIST = "gid_list"
        const val KEY_ACTION = "action"
        const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
        private const val ID_DOWNLOADING = 1
        private const val ID_DOWNLOADED = 2
        private const val ID_FATAL = 3
        private const val DELAY = 1000L // 1s
        private val sItemStateArray = LongSparseArray<Boolean>()
        private val sItemTitleArray = LongSparseArray<String>()
        private var sFailedCount = 0
        private var sFinishedCount = 0
        private var sDownloadedCount = 0

        context(ctx: Context)
        inline fun startService(action: String, intentBuilder: Intent.() -> Unit = {}) {
            val intent = Intent(ctx, DownloadService::class.java).setAction(action).apply(intentBuilder)
            ContextCompat.startForegroundService(ctx, intent)
        }

        context(_: Context)
        fun startDownload(info: BaseGalleryInfo, label: String? = null) = startService(ACTION_START) {
            putExtra(KEY_GALLERY_INFO, encodeToSavedState(info))
            label?.let { putExtra(KEY_LABEL, it) }
        }

        context(_: Context)
        fun startRangeDownload(gidList: LongArray) = startService(ACTION_START_RANGE) {
            putExtra(KEY_GID_LIST, gidList)
        }

        fun clear() {
            sFailedCount = 0
            sFinishedCount = 0
            sDownloadedCount = 0
            sItemStateArray.clear()
            sItemTitleArray.clear()
        }
    }
}
